Attribution pipeline (Rust port of feat/node-attribution prototype)#190
Open
shikokuchuo wants to merge 50 commits into
Open
Attribution pipeline (Rust port of feat/node-attribution prototype)#190shikokuchuo wants to merge 50 commits into
shikokuchuo wants to merge 50 commits into
Conversation
Lands the plan at claude-notes/plans/2026-05-06-attribution-pipeline.md and the Phase 0 TDD baseline: a compile-only skeleton of the attribution module (types, builder, traits, palette, mode, providers) plus 46 tests pinning the contract for the next phases. 38 are red against `unimplemented!()` bodies that Phase 1-6 will turn green; 8 are green immediately as regression pins (transport JSON round-trip, SourceInfo chain resolution, RenderContext-default state, attribution-off HTML body baseline snapshot). Visible surface added: `quarto_core::attribution::*` module, `AttributionGenerateTransform` / `AttributionRenderTransform` (unregistered in the pipeline until Phase 2/4c), three new `RenderContext` fields (`attribution_provider`, `attribution_data`, `format_options`), `BinaryDependencies.git`, and the WASM stub `parse_qmd_to_ast_with_attribution` next to the existing `parse_qmd_to_ast`. No production code path changes behavior yet.
Updates the Phase 0 sections (unit-test files, snapshot tests, work items checklist) to reflect what landed in b2ee6e70. Also notes where Phase 0 wrote slightly less than the original plan (no attribution_generate__* snapshots — those need a non-unimplemented transform to render against — and only one HTML baseline rather than off+on, because the on case is exercised by Phase 4b's structured DOM assertions).
Replace the Phase 0 unimplemented!() bodies with the real impls: AttributionDataBuilder interns actors once and Arc::clone-s thereafter; AttributionMap::query_byte_range uses partition_point + a forward walk to pick the most-recent overlapping run; PreBuiltAttributionProvider:: build re-interns through the builder so transport JSON round-trips restore Arc<str> ptr-equality; format_supports_attribution matches FormatIdentifier::Html (covers html, q2-debug, extension HTML); from_config_value reads meta.attribution.identities into an IdentityMap with fresh Arc keys so Phase 2's merge can preserve provider keys on collision. Turns Phase 0 tests #1 + #2 (10 cases) green; the 8 regression pins stay green.
Replace the Phase 0 unimplemented!() body in
AttributionGenerateTransform with the four-rule skip ladder
(format_supports_attribution → is_feature_disabled → provider present
→ build + merge). Identity merge preserves the provider's Arc<str>
keys on collision (so the writer-side ptr_eq interning invariant
survives) and drops non-colliding user-only keys (which would be
invisible at the writer and dead weight in the map).
Register the stage at the tail of the Navigation Phase in
build_transform_pipeline, immediately after FooterRenderTransform and
before LinkRewriteTransform opens the Finalization Phase. With no
provider installed the transform is a no-op, so the
attribution_off_html_baseline regression snapshot stays green.
Also fix one Phase 0 test fixture:
Format::from_format_string("native") doesn't exist in
FormatIdentifier; "pdf" is any-non-HTML and proves the skip ladder
bails before the PanicProvider.
quarto-core: +6 green tests (all 6 in tests/attribution_generate.rs);
remaining 25 reds are explicit unimplemented!() panics owned by
Phases 3a/3b/3c/4/6.
GitBlameProvider shells out to `git blame --porcelain`, parses + builds runs, and synthesizes identities; PreBuiltAttributionProvider was already in place. `--attribution=git|off` is plumbed through RenderArgs → RenderToFileOptions → outer RenderContext and bridged via StageContext into the inner ctx that AstTransformsStage hands to AttributionGenerateTransform. parse_qmd_to_ast_with_attribution lives in wasm-quarto-hub-client; parse_qmd_to_ast collapses to a one-line delegating wrapper. Phase 4's AttributionRenderTransform / writer emission is still red, so 10 attribution_render + cli_e2e tests stay red until that lands; the other 8846 workspace tests pass.
…ssion AttributionRenderTransform builds the actors table and per-node lookup on ctx.format_options; HTML writer emits data-attr-actor/time/name/color on every Block and Inline with a hit, gated independently from data-sid/data-loc. Off-path snapshot stays byte-identical; CLI E2E yields the expected alice/bob data-attr-actor values. Prose-coalescing and the q2-debug JSON wire shape are deferred to Phase 5.
…e siblings
JSON writer emits astContext.attribution (sparse {s, actor, time} array)
and astContext.attributionActors (actor → {name, color} table) when
AttributionRenderTransform populates ctx.format_options.json. Off-path
output is byte-identical to today's (Phase 0 test #10 backing). Three
new pampa tests pin the wire shape; the existing 8859 workspace tests
all still pass.
Implementation:
- Re-key attribution_by_node by &SourceInfo ptr instead of &Block/&Inline
ptr. Lets the JSON writer's existing helpers (already carrying
&SourceInfo) look up attribution with no caller refactor; HTML writer's
lookups updated symmetrically.
- New JsonAttributionRecord / JsonAttributionIdentity types on
pampa::writers::json (writer-local, mirror the html.rs pattern).
- JsonWriterContext gains an attribution_records accumulator and a
maybe_record_attribution_for(&SourceInfo, s_id) hook called at every
stream-write site that interns.
- WASM parse_qmd_to_ast_with_attribution forwards
ctx.format_options.json into the JsonConfig.
Phase 5 TS-side palette helpers:
- fnv1aHex8 sibling alongside actorColor in useReplayMode.ts. UTF-8-byte
iteration matches the Rust palette.rs::fnv1a_hex8 bit-for-bit (pinned
by three new vitest assertions using the same reference vectors as
palette.rs::tests::fnv1a_hex8_known_vectors).
Phase 5 hub-client checklist items (useAttribution refactor, wasmRenderer
shim, ReactAstDebugRenderer consume, browser verify) are still ahead.
Builds the hub-client producer half of the q2-debug attribution pipeline. Data path is end-to-end functional: `useAttribution` replays Automerge history, translates char offsets to UTF-8 byte offsets, and emits the JSON payload `PreBuiltAttributionProvider` consumes. The WASM entry returns an AST with `astContext.attribution` + `astContext.attributionActors` for the q2-debug renderer. New files (producer-only — no consumer-side ports per the plan): - `hub-client/src/services/attribution-runs.ts` — RLE producer ported from `feat/node-attribution` with self-contained types, plus new `buildCharToByteMap` / `runsCharToByteOffsets` for the wire translation. Six new vitest assertions cover the char→byte cases (ASCII, 2-byte, 3-byte CJK, 4-byte surrogate-pair). - `hub-client/src/hooks/useAttribution.ts` — the producer hook. Owns the build/update lifecycle, identity fallback (every actor in `runs` ends up in `identities` at the wire, satisfying the Phase 6 producer invariant), and JSON serialisation. Returns `null` when `enabled: false`, in which case the caller stays on the byte-identical no-attribution path. Wire-up: - `wasmRenderer.ts` exposes `parseQmdToAstWithAttribution(content, attributionJson)`. `parseQmdToAst(content)` now delegates with `null` — same code path the Rust side asserts byte-identical via Phase 0 test #10. - `ReactPreview` calls `useAttribution(...)` (with `enabled: false` for now — the Authorship toggle UI is the Phase 5c deliverable) and routes through `parseQmdToAstWithAttribution`. With the hook disabled the payload stays `null` and the pipeline behaviour is unchanged. Phase 5c (deferred to a follow-up): - Authorship toggle UI in Editor / sidebar; flip `enabled: false → enabled: <toggleState>`. - `ReactAstDebugRenderer` colour application (read `astContext.attribution` + `attributionActors`, apply per-node colour). The implementation branch carries no consumer-side attribution code today, so this is greenfield renderer work rather than a refactor. - End-to-end browser verification — meaningful only once the toggle + renderer surface exist. Verified: `cargo xtask verify --skip-hub-build` (8859/8859 + lint + fmt + `-D warnings`); `cd hub-client && npm run build:all` (TS build + WASM build + vite); `npm run test:ci` (74/74 hub-client tests).
Extract `attribution_from_porcelain` from `GitBlameProvider::build` so the porcelain → AttributionData path is testable without a `RenderContext`. Replace placeholder test #12 with two fixture-driven producer-invariant assertions that pin the deterministic colour for `alice@example.com` and `bob@example.com` and pin the Arc-interning invariant between run actors and identity-map keys. Add `docs/authoring/attribution.qmd` covering `--attribution=git`, the YAML form, CLI/YAML resolution, identity overrides, the emitted `data-attr-*` attributes, and the privacy note. Register it under a new "Attribution" navbar entry.
cargo build / nextest / xtask verify all green (treesitter steps skipped — host lacks the CLI; attribution touches no grammar). CLI end-to-end on a two-author git fixture emits the expected data-attr-* markup with Phase 6-pinned colours. Hub-client browser end-to-end deferred: ReactPreview enables attribution behind enabled:false until Phase 5c (toggle UI + ReactAstDebugRenderer colour application) lands. Recorded alongside the other deviations from the TS prototype (prose coalescing, q2-debug JSON wire shape, binary name).
write_inlines now collapses contiguous prose inlines (Str/Space/ SoftBreak/LineBreak) that share the same (actor, time) lookup into one outer <span data-attr-*> wrapper. Structured inlines (Code, Emph, Strong, Link, Span, Math, ...) break the run and carry their own attribution on their element tag. When include_source_locations is on, per-Str <span data-sid=...> nest inside the outer wrapper. Pinned by 4 cases in crates/pampa/tests/attribution_html_coalescing_test.rs (basic coalescing, actor-change break, structured-inline break, src-locs-off composition). The existing Phase 0 scaffolds at crates/quarto-core/tests/attribution_render.rs:289, 326, 376 now point at the writer-level coverage rather than the deferred-TODO. Off-path output stays byte-identical (attribution_off_html_baseline still green); workspace tests 8865/8865 pass.
…tributionEnabled preference `usePreference` previously gave each hook instance its own useState that only updated when *its own* setter fired, leaving sibling consumers stale until a manual page refresh. The native `storage` event was not enough — it only fires in OTHER windows, not the one that wrote. Also declares `attributionEnabled: z.boolean().default(false)` in the preferences schema, since the cross-instance reactivity test uses it as its fixture and the toggle commit below consumes it. The `.default(false)` keeps existing localStorage entries forward-compatible.
…impl v1 drew the writer-side hand-off as a position-indexed slice. The shipped code uses a SourceInfo-pointer-keyed HashMap (with the slice demoted to a non-empty-when-on test invariant), format_options is a struct not enum, and pampa writers carry their own record types. v2 redraws sections 6-8 around that and refreshes line anchors and the q2 binary name.
…view call site `AttributionRenderTransform` added `attribution_by_node` and `attribution_actors` to `JsonConfig`; q2-preview's writer call site (landed on main during this rebase) didn't know about them. Use `..JsonConfig::default()` to pick up `None` defaults — q2-preview stays attribution-off, matching its existing behaviour.
…ework/+q2-debug/ After main carved the renderer into framework/ + q2-debug/ (Plan 2pre 2.14, commit c245435) the original Phase 5c diff no longer applies. Re-target the colouring against the new split: - framework/AttributionLookupContext.tsx: NodeAttributionIdentity, AttributionLookupContext, useNodeAttribution hook. Format-agnostic so q2-preview can opt in later. - framework/Ast.tsx: read astContext.attribution + astContext.attributionActors, build the Map once per render (useMemo), provide via AttributionLookupContext.Provider. Off path leaves the context value as null; consumers short-circuit. - q2-debug/dispatchers.tsx: Block/Inline wrap their dispatched output in .q2-attr-wrap (div/span) with data-sid + inline color when useNodeAttribution returns non-null. Off path skips the wrap entirely. - q2-debug/attribution.tsx: AttributionBadge, attributionStyles CSS, formatRelativeTime helper. - q2-debug/components.tsx: AstRenderer injects the stylesheet and installs event-delegated mouseover/mouseout on .q2-attr-wrap[data-sid] to render a single floating badge near the hovered element. Pinned by attribution.integration.test.tsx (4 cases mounting the framework Ast with q2DebugRegistry): off path, on path, hover badge, ghost actor with no entry in attributionActors.
… identity threading
Closes the Phase 5c port. Restores the bits dropped from the rebase
that wire the renderer-side colouring (commit a0416b23) into the
user-facing toggle and the Automerge identity table:
- services/preferences/schema.ts: attributionEnabled boolean with
z.default(false) so existing localStorage entries forward-compat
without resetting other preferences.
- components/tabs/SettingsTab.tsx: "Authorship" checkbox under
Settings → Preview.
- components/render/ReactPreview.tsx: read the preference via
usePreference, drive useAttribution({ enabled }); accept an
optional `identities` prop and pass it as the identities table
so profile-metadata names win over the fnv1a fallback when
available.
- components/render/PreviewRouter.tsx: pass-through for the new
prop (Preview doesn't take it).
- components/Editor.tsx: thread the existing `identities`
Automerge-presence map down to PreviewRouter.
- hooks/usePreference.test.tsx: re-target the cross-instance
reactivity test at attributionEnabled (its original Phase 5c
fixture), undoing the temporary errorOverlayCollapsed
decoupling done while the preference was absent.
Off path stays byte-identical: when the toggle is off,
useAttribution short-circuits to a null payload and the WASM call
falls through to the unflagged q2-debug path.
735e3df to
df20065
Compare
The q2-debug and q2-preview iframe entries gated UPDATE_AST on componentsLoading via a 50-ms setInterval. Two UPDATE_ASTs arriving during a LOAD_CUSTOM_COMPONENTS could resolve out of arrival order because each waiter had its own interval phase — so the no-attribution AST overwrote the with-attribution one, and Authorship never showed on first render for large files with `render-components: - html.tsx`. Replaced with a single shared promise; await-continuations preserve microtask-FIFO order. Extracted to iframeMessageDispatch.ts and regression-tested with a fake-timer reproduction of the legacy race.
Phase 0-2 of `2026-05-13-q2-preview-attribution.md`: thread `ctx.format_options.json.attribution_*` into the q2-preview JSON writer, install `PreBuiltAttributionProvider` from a renderer-side builder, and expose `render_page_in_project_with_attribution`. Also fixes an underlying gap — `run_pipeline` was not transferring `stage_ctx.format_options` back to the outer ctx, so attribution data populated inside `AstTransformsStage` was invisible to the JSON writer that runs outside the pipeline.
…2-preview Threads the `useAttribution` payload through `doRender`'s q2-preview branch so the resulting AST JSON carries `astContext.attribution*` when Authorship is on. Off-path is byte-identical via the wrapper.
…review/ `q2-debug/attribution.tsx` moves into `framework/` so both formats share the badge + stylesheet. q2-preview's `Block`/`Inline`/`CustomBlock`/ `CustomInline` dispatchers now call `useNodeAttribution` and wrap on hit; `PreviewDocument` injects `attributionStyles` and event-delegated hover handlers around the document root. Off-path the DOM is byte-identical to today's. Sibling of 10dd3cf.
Three call sites (HTML body stage, q2-preview JSON writer, q2-debug WASM entry) all converted `Option<Arc<HashMap<..., quarto_core::Attribution*>>>` into the equivalent pampa-side record types via hand-written `map(|m| m.iter().map(...).collect())` blocks. The blocks were 40-50 LOC each and structurally identical; adding a field to either side of the type pair required three matching edits. Extract the per-field clone into `quarto_core::attribution::pampa_bridge` exposing `html_attribution_fields` and `json_attribution_fields`. Each call site collapses to one call; the bridge module is the single point of edit for future field additions. Net: -121 LOC at call sites, +116 LOC in the new module. No behavior change; off-path callers still receive `(None, None)`.
Six dispatcher sites (q2-debug Block/Inline + q2-preview Block/Inline/
CustomBlock/CustomInline) each carried a 9-line "if attribution wrap
in div/span.q2-attr-wrap with data-sid + style" block. Two document-
root components (q2-debug AstRenderer, q2-preview PreviewDocument)
each carried a ~70-line copy of the lookup-context + useRef +
hovered-state + mouseover/mouseout + style+overlay state machine.
Extract two reusables into framework/attribution.tsx:
- <AttributionWrap node={...} as="div|span">: pass-through off path,
wraps in .q2-attr-wrap otherwise. The six dispatchers each collapse
to a single line.
- useAttributionHover(): returns { enabled, hostProps, stylesheet,
overlay }. Off path the props are inert / null, so consumers
spread/render unconditionally and the DOM stays byte-identical to
pre-attribution.
Net: -59 LOC across the six call sites. No behavior change; the two
attribution integration tests (q2-debug + q2-preview) and the rest of
the hub-client suite (84/84) pass unchanged.
One DOM-position nuance: q2-debug's AstRenderer previously rendered
the AttributionBadge overlay *inside* its `pandoc-content-debug` div;
the hook now returns it as a sibling so q2-debug and q2-preview share
identical structure. The badge has `position: fixed`, so the visual
effect is unchanged.
The `--attribution` CLI flag previously declared `value_parser = ["git", "off"]` and routed through a hand-written `parse_attribution_mode(&str) → Option<AttributionMode>` mapper. Two declarations of the value list (clap's value_parser literal and the match arms) had to stay in sync; adding a third mode would silently break the resolver if either was forgotten. Gate a `clap::ValueEnum` derive on `AttributionMode` behind a new `clap` cargo feature on `quarto-core`, enabled by the `quarto` CLI crate. Other consumers (notably the WASM build) keep clap out of their dep tree. The CLI arg becomes `Option<AttributionMode>` directly; `parse_attribution_mode` is gone. `q2 render --help` still shows `[possible values: off, git]`, generated by clap from the enum variants.
…meta The free function in `attribution/types.rs` parses `meta.attribution.identities` into an `IdentityMap`. The old name read as a generic "from any ConfigValue" converter and collided in search results with the unrelated `Navbar::from_config_value` / `Sidebar::from_config_value` / `PageNavigation::from_config_value` methods elsewhere in the crate. The new name says what the function actually returns and what it reads from.
`actorColor` and `fnv1aHex8` are pure utilities with a strict Rust-sibling drift contract (`crates/quarto-core/src/attribution/ palette.rs`). They previously lived inside the React-bearing `hooks/useReplayMode.ts`, which forced `useAttribution.ts` to import them from a sibling hook module. Move both functions and their drift-mitigation tests to `utils/palette.ts` + `utils/palette.test.ts` so the two real call sites (`useReplayMode` / `useAttribution`) and `ReplayDrawer.tsx` import from a single non-React module. No behavior change; both helpers are byte-for-byte identical and the test vectors are unchanged.
The `~70-line` text in the b6b03dd entry tripped the qmd parser's subscript rule (`~...~`) and broke `changelogRender.wasm.test.ts`. Rephrase to drop the literal tilde.
The builder previously exposed `intern_actor(&str) -> Arc<str>` and
required callers to thread the returned `Arc` through `push_run` /
`set_identity`. This was a doc-only contract; a misuse — passing a
freshly-allocated `Arc::from(actor)` to `push_run` while the builder
held a separate `Arc` for the same string — would silently break the
writer-side `Arc::ptr_eq` invariant between `AttributionRun.actor`
and the matching `IdentityMap` key.
Make `intern_actor` private. The public API now takes `&str`
everywhere; the builder interns each distinct string exactly once
internally. Cascading simplifications:
- `attribution_from_porcelain` (git_blame.rs) collapses from a
two-pass walk with a parallel `seen: HashMap<String, Arc<str>>`
into a single pass using the new `set_identity_if_absent`
helper.
- `PreBuiltAttributionProvider::build` drops its two pre-intern
lookups.
- Four test files lose their `let alice = b.intern_actor("alice");`
boilerplate.
No behavior change; the writer-side invariant is now enforced by
construction rather than by convention.
vezwork
reviewed
May 13, 2026
vezwork
reviewed
May 13, 2026
Member
vezwork
left a comment
There was a problem hiding this comment.
looking good from my perspective
Adds a copy-pasteable CSS+JS overlay to attribution.qmd that reproduces the hub-client's Authorship hover badge against the static --attribution=git output, and fixes the data-attr-time units note (git emits seconds, Automerge ms).
Pins the full data-attr-* contract (seconds-unit time, mail-local-part display name, distinct per-actor hsl colours) on the existing two- author cli fixture, refreshes the stale Phase 0 doc-comment, and drops the unix-only ignore so Windows CI exercises the path too.
Pins the seconds-vs-ms heuristic at the 1e12 boundary and the per-
bucket wording ("just now", "Nm ago", "Nh ago", "Nd ago"), mirroring
on the consumer side the unit contract the Rust CLI test pins on the
producer side.
Pins the pure-function half of useAttribution: char→byte translation for ASCII and multi-byte UTF-8 source text, profile-vs-fallback identity resolution, and the Phase 6 producer invariant (every actor in `runs` has an identity at the wire — protects against the writer's `<unknown>` / `#888888` warning-path placeholders). Lifecycle paths (debounce, AbortController, HistoryCompactedError recovery) remain out of scope; they need an Automerge fixture.
`--attribution=git` (or `attribution: git` YAML) now ships an inline
`<style>` + `<script>` pair so attributed prose carries a dotted
underline, body text painted in the author's colour, and a hover
badge — no more copying the 70-line snippet from
docs/authoring/attribution.qmd. Suppress with
`attribution: { viewer: false }`; the per-node wrappers stay either
way.
`resources/attribution/viewer.css` is the single source of truth: the
CLI `include_str!`s it; the hub-client reads it through a
virtual-module Vite plugin (a plain `?raw` import of an out-of-tree
path returns empty under Vitest). The CLI's viewer JS does the same
`querySelectorAll('[data-attr-actor]')` + `style.color` pass that
hub-client's `AttributionWrap` already did, so static HTML output
looks the same as the live hub preview.
No snapshot files affected (off path is byte-identical; no on-path
snapshot existed beforehand). See
claude-notes/plans/2026-05-14-attribution-auto-viewer.md.
…shness
The git blame parser previously read only `author-time`, so any commit
with a back-dated author (`git commit --date=PAST`, rebase, cherry-pick,
amend) made the rendered wrapper's `data-attr-time` reflect the
back-date rather than when the line landed in the branch. The hover
badge consequently showed "910 days ago" for a line just committed.
The fix: parse `committer-time` instead and route `BlameRun.time`
through it. Matches the doc wording ("most recent commit") and the
hub-client's Automerge time semantics (edit time, not original
authorship time). `BlameLine` drops `author_time` entirely — nothing
read it outside the parser, and `git blame --porcelain` always emits
committer-time, so the field was dead weight. Author identity
(`author-mail`) is unchanged.
Regression coverage:
`parse_uses_committer_time_for_run_time_even_with_backdated_author`
(parser-level) and `cli_attribution_git_emits_committer_time_for_backdated_commit`
(end-to-end through the CLI binary with a `GIT_AUTHOR_DATE`
back-dated to 2023 and `GIT_COMMITTER_DATE` set to a distinct value).
HTML wrappers carry only `data-attr-actor` + `data-attr-time` now;
`AttributionViewerTransform` emits one `[data-attr-actor="<id>"] {
--attr-color; --attr-name; }` rule per actor into the head `<style>`
block, and the base paint rule in `viewer.css` consumes
`var(--attr-color)` via the cascade. `viewer.js` drops the
querySelectorAll colour pass and reads identity from computed style
for the hover badge. Saves O(N) inline bytes per wrapper, unifies the
HTML/JSON wire shapes, gives users a single override surface
(`meta.attribution.identities` → one CSS rule).
Replace the unconstrained 360-hue HSL formula with a 10-bucket modular index into Paul Tol's "Muted" qualitative palette — colour-blind safe across all three deficiencies, perceptually uniform brightness on white, widely recognised in scientific publishing. Same `fnv1a_hex8` hash for git emails; same `actor.slice(0, 6)` mod for Automerge IDs; only the tail mapping changes. Per-actor pins (alice/bob) and the cross-crate parity vectors are updated on both Rust and TS sides.
The wrapper-shape section, viewer description, and "customizing presentation" guidance still described the old four-attr contract (`data-attr-name` / `data-attr-color` per node) and the old HSL palette. Bring the doc up to date: per-node attrs shrink to `data-attr-actor` + `data-attr-time`, identity moves to one `[data-attr-actor="…"]` CSS rule per author publishing `--attr-color` / `--attr-name`, and the colour story switches to Paul Tol's Muted palette with a link to the source notes.
Mirror the CLI refactor (30d4692) on the hub-client side. `AttributionWrap` drops the inline `style={{ color }}` and adds `data-attr-actor={record.actor}`; `useAttributionHover` extends its `<style>` block with one `[data-attr-actor="…"] { --attr-color; --attr-name; }` rule per distinct actor in the lookup. The `viewer.css` cascade then paints each wrap via `var(--attr-color)`. Badge stays React-driven from the lookup context — only paint moves to CSS. The wire (`astContext.attribution` + `astContext.attributionActors`) is unchanged.
The hub-client refactor (6440445) added `data-attr-actor` to React wraps so the colour rule would apply via cascade, but that also made the `[data-attr-actor]` underline rule match — bringing a CLI-only discoverability cue into the editing preview. Split the rule: colour stays cascaded to both surfaces; underline + cursor key on `[data-attr-actor]:not(.q2-attr-wrap)` so only the CLI wraps (bare `<span data-attr-actor=…>`) get them.
…istence Authorship is a session-scoped inspection mode, not a setting. Lift it out of the persisted `attributionEnabled` preference and the Settings → Preview list (three-click activation, no in-preview state cue) into a pill toggle in the replay bar that's always visible whenever a file is open. State lives as `useState` in `Editor.tsx` and resets on reload — a previously-curious view no longer bleeds into the next session. The pill sits flush-right in both the collapsed (28px) and expanded (72px) replay-bar states, reusing the `--editor-accent-*` and `--editor-success-*` tokens so it inherits the existing visual idiom without coining a new control. Semantic grouping with the replay UI matches the data: both surfaces share the per-actor colour palette and both are authorship-inspection tools, just at different grains. `attributionEnabled` removed from `preferences/schema.ts` (zod schema strips unknown keys, so stale localStorage entries are tolerated without a migration). The Authorship checkbox is removed from `SettingsTab`. `usePreference.test.tsx` switched to `errorOverlayCollapsed` as its generic probe key.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds per-node authorship attribution to Quarto renders, sourcing authors from
git blameon the CLI and from Automerge edit history in the hub. The shape on both surfaces is the same: each wrapped node carriesdata-attr-actor(the author email or Automerge actor) anddata-attr-time(a Unix timestamp), and one CSS rule per author publishes the display name and colour via the cascade.On the CLI,
quarto render report.qmd --attribution=git(orattribution: gitin YAML) is all it takes. Contiguous same-author prose coalesces into a single outer<span>. A small<style>+<script>pair is auto-injected so a vanilla render looks attributed out of the box — author-coloured text, a dotted underline, and a floating badge on hover with the contributor's name and a relative timestamp. Colours come from Paul Tol's "Muted" 10-colour, colour-blind-safe palette; themes can recolour any author with a single rule, andattribution: { viewer: false }strips the viewer if you want just thedata-attr-*contract.In the hub, an "Authorship" pill in the replay-drawer bar toggles the same overlay on the live document — session-only, no persistence, one click. Both q2-debug and q2-preview share a single
<AttributionWrap>component and hover hook inframework/. Off-path output stays byte-identical: nothing is emitted unless a producer is installed (the CLI flag, or the pill in the hub), and one toggle gates everything.User docs:
docs/authoring/attribution.qmd. Design rationale and deferred-work tally:claude-notes/plans/2026-05-06-attribution-pipeline.md. Note that author emails are written into the rendered HTML — same exposure surface as a public commit log — which is called out in the docs.